# coding: utf-8

import bz2
import zlib
import glob
import math
import random
from operator import concat

def compress_bz2(sequence):
    return bz2.compress(reduce(concat, sequence), 9)
    
def compress_zlib(sequence):
    return zlib.compress(reduce(concat, sequence), 9)
    
def read_files(filenames):
    # this function accepts a list of filenames
    # or a pathname pattern string
    if type(filenames) == str:
        filenames = glob.glob(filenames)
        
    return [open(fn).read() for fn in filenames]

class Table(object):
    def __init__(self, sequence, compress, winsize=1):
        self.sequence = sequence
        self.compress = compress
        self.winsize = winsize
        
        self.table = {}
        for w in xrange(1, winsize+1):
            for i in xrange(len(sequence)):
                s = tuple(sequence[i:i+w])
                self.table[s] = float(len(compress(s)))
                
        self.means = {}
        for w in xrange(1, winsize+1):
            l = [self.table[k] for k in self.table if len(k) == w]
            self.means[w] = sum(l)/len(l)
            
        self.stddevs = {}
        for w in xrange(1, winsize+1):
            l = [(self[k]-self.means[w])**2 for k in self.table if len(k) == w]
            self.stddevs[w] = math.sqrt(sum(l)/len(l))
                
    def __getitem__(self, key):
        if type(key) == list:
            key = tuple(key)
        elif type(key) != tuple:
            key = (key,)
        return self.table[key]
        
    def mean(self, order):
        return self.means[order]
        
    def stddev(self, order):
        return self.stddevs[order]
    
    def ncd(self, x, y):
        Cxy = float(self.table[(x,y)])
        Cx = float(self.table[(x,)])
        Cy = float(self.table[(y,)])
        return (Cxy - min(Cx, Cy)) / max(Cx, Cy)

    def ncd_two_sequences(self, x, y):
        Cxy = float(len(this.compress(reduce(operator.concat, operator.concat(x,y)))))
        Cx =  float(len(this.compress(reduce(operator.concat, x))))
        Cy =  float(len(this.compress(reduce(operator.concat, y))))
        return (Cxy - min(Cx, Cy)) / max(Cx, Cy)



class VotingExperts(object):
    def __init__(self, sequence, winsize, compress):
        self.sequence = sequence
        self.winsize = winsize
        self.table = Table(sequence, compress, winsize)
        
#    def expert1(self, *elems):
#        mean = self.table.mean(len(elems))
#        stddev = self.table.stddev(len(elems))
#        return (self.table[elems] - mean) / stddev
#        
#    def expert2(self, *elems):
#        return self.table[elems] - self.table[elems[:-1]]

    def get_std_int_entropy(self, seg):
        mean = self.table.mean(len(seg))
        stddev = self.table.stddev(len(seg))
        return (self.table[seg] - mean) / stddev

    def expert1(self, lst):
#        if len(lst) != self.winsize:
#            print "YO WE GOT PROBLEMS in EXPERT 1: WINDOW LENGTH != SELF.WINSIZE"
#            return
#        score = [0] * (self.winsize + 1)
#        votes = [False] * (self.winsize + 1)
#        for i in xrange(2, self.winsize):
#            score[i-1] = self.get_std_int_entropy(lst[:i]) #+ self.get_std_int_entropy(lst[i:])
#        score[self.winsize] = self.get_std_int_entropy(lst)
#
#        cutPos = 1
#        maxEnt = score[cutPos]
#        for j in xrange(1, len(score)):
#            if score[j] > maxEnt:
#                maxEnt = score[j]
#                cutPos = j
#        votes[cutPos] = True
#        return votes

        # trying out min with offset window size
        if len(lst) != self.winsize:
            print "SANITY CHECK FAILED in EXPERT 1: WINDOW LENGTH != SELF.WINSIZE"
            return
        score = [0] * (self.winsize + 1)
        votes = [False] * (self.winsize + 1)
        for i in xrange(1, self.winsize):
            score[i] = self.get_std_int_entropy(lst[:i]) + self.get_std_int_entropy(lst[i:])
        # have the std entropy of the whole sequence be stored in the last boundary, and nothing stored in 0th boundary
        score[self.winsize] = self.get_std_int_entropy(lst) 
        cutPos = 1
        minEnt = score[cutPos]
        for j in xrange(1, len(score)): # this doesn't look at the first boundary, which we know is 0
            if score[j] < minEnt:
                minEnt = score[j]
                cutPos = j
        votes[cutPos] = True
        return votes

    def expert2(self, lst):
        if len(lst) != self.winsize:
            print "SANITY CHECK FAILED in EXPERT 2: WINDOW LENGTH != SELF.WINSIZE"
            return
        score = [0] * (self.winsize + 1)
        votes = [False] * (self.winsize + 1)
        for j in xrange(1, self.winsize):
            score[j] = self.table[lst[:j+1]] - self.table[lst[:j]]
        
        cutPos = 1
        maxEnt = score[cutPos]
        for j in xrange(1, len(score) - 1):
            if score[j] > maxEnt:
                maxEnt = score[j]
                cutPos = j
        votes[cutPos] = True
        return votes
        
    def vote(self):
        random.seed();
        ws = self.winsize
        seq = self.sequence
        votes = [0] * (len(seq)+1)
        # traverse the sequence        
        for i in xrange(len(votes)-ws):
            votes1 = self.expert1(seq[i:i+ws])
            votes2 = self.expert2(seq[i:i+ws])
#            print votes1
#            print votes2
            for j in xrange(ws+1):
                votes[i+j] += votes1[j] + votes2[j]

        return votes[1:-1]

#    def vote(self):
#        random.seed();
#        ws = self.winsize
#        seq = self.sequence
#        votes = [0] * (len(seq)-1)
#        
#        # traverse the sequence        
#        for i in xrange(len(seq)):
#            # win will be successive chunks of size ws
#            win = seq[i:i+ws]
#            # this will only work if ws >= 2
#            if len(win) >= 2:
#                score1 = [0] * (len(win)-1)
#                score2 = [0] * (len(win)-1)
#                for j in xrange(2, len(win)+1):
#                    score1[j-2] = self.expert1(*win[:j])
#                for j in xrange(2, len(win)+1):
#                    score2[j-2] = self.expert2(*win[:j])
#                if i < len(votes)-1:
#                    votes[i+score1.index(min(score1))] += 1
#                    votes[i+score2.index(max(score2))] += 1
#        return votes
        
    def threshold(self, th):
        return [(1 if x > th else 0) for x in self.vote()]

    def segment(self, th):
        threshold = self.threshold(th)
        start = 0
        retval = []
        for i,v in enumerate(threshold):
            if v:
                retval.append(self.sequence[start:i+1])
                start = i + 1
        retval.append(self.sequence[start:])
        return retval
        
    def ncd(self):
        # see The similarity metric, Li et al. 
        retval = []
        for i in xrange(1, len(self.sequence)):
            retval.append(self.table.ncd(self.sequence[i-1], self.sequence[i]))
        return retval

